#!/bin/bash
#
# Dart Version Manager
# author: Chris Bracken <chris@bracken.jp>

DVM_ROOT="${DVM_ROOT:-$HOME/.dvm}"
DVM_VERSION=$(cat "$DVM_ROOT/VERSION")

_dvm_usage() {
  echo "usage: dvm <command> [<args>]"
  echo ""
  echo "Commands:"
  echo "   alias      Manage Dart version aliases"
  echo "   help       Display usage"
  echo "   implode    Delete dvm and all installed Dart versions"
  echo "   install    Install a Dart version"
  echo "   list       List installed Dart versions"
  echo "   listall    List all available Dart versions (--dev for dev channel)"
  echo "   upgrade    Upgrade dvm to the latest version"
  echo "   use        Select a Dart version to use (--default to set as default)"
  echo "   version    Display the dvm version number"
  return 1
}

_dvm_create_dirs() {
  mkdir -p "$DVM_ROOT/darts" > /dev/null 2>&1
  mkdir -p "$DVM_ROOT/environments" > /dev/null 2>&1
}

# Removes $1 from PATH.
_dvm_path_remove() {
  PATH=$(echo $PATH | sed "s:$1::;s/::/:/;s/:$//")
}

# Adds or moves $1 to beginning of PATH.
_dvm_path_prepend() {
  _dvm_path_remove "$1"
  if [ -d "$1" ]; then
    export PATH="$1:$PATH"
  fi
}

_dvm_doctor() {
  # Check for unzip tool.
  unzip_path="$(which unzip)"
  if [[ ! -x "$unzip_path" ]]; then
    echo "ERROR: dvm requires the 'unzip' tool, which was not found."
    echo "To install unzip:"
    echo "  Debian/Ubuntu: sudo apt install unzip"
    echo "  RedHat/Fedora: sudo dnf install unzip"
    echo "  OpenSUSE:      sudo zypper install unzip"
    echo "  Arch/Manjaro:  sudo pacman -S unzip"
    return 1
  fi
}

_dvm_alias_usage() {
  echo "usage: dvm alias <create|update|delete|list> [<args>]"
}

dvm_alias_list() {
  find "$DVM_ROOT/darts" -maxdepth 1 ! -path "$DVM_ROOT/darts" -type l -exec basename "{}" \; | sort
}

_dvm_alias_set() {
  # ensure target exists
  if [[ "$2" == "--path" ]]; then
    local abs_path="$3"
    if [[ "$abs_path" != /* ]]; then
      abs_path="$PWD/$3"
    fi
    if [[ ! -e "$abs_path" ]]; then
      echo "ERROR: target path $3 does not exist."
      return 1
    fi
    rm -f -- "$DVM_ROOT/darts/$1"
    ln -s -- "$abs_path" "$DVM_ROOT/darts/$1"
  else
    if [[ ! -e "$DVM_ROOT/darts/$2" ]]; then
      echo "ERROR: target version $2 does not exist."
      return 1
    fi
    rm -f -- "$DVM_ROOT/darts/$1"
    ln -s -- "$2" "$DVM_ROOT/darts/$1"
  fi
}

dvm_alias_update() {
  if [[ $# < 2 || "$2" == "--path" && $# < 3 ]]; then
    echo "usage: dvm alias update <alias> [--path] <version>"
    return 1
  fi

  # check for existing alias/version
  if [[ ! -h "$DVM_ROOT/darts/$1" ]]; then
    echo "ERROR: no such alias $1."
    return 1
  fi

  _dvm_alias_set "$@"
}

dvm_alias_create() {
  if [[ $# < 2 || "$2" == "--path" && $# < 3 ]]; then
    echo "usage: dvm alias create <alias> [--path] <version>"
    return 1
  fi

  # check for existing alias/version
  if [[ -h "$DVM_ROOT/darts/$1" ]]; then
    echo "ERROR: alias $1 already exists."
    return 1
  elif [[ -d "$DVM_ROOT/darts/$1" ]]; then
    echo "ERROR: version $1 already exists."
    return 1
  fi

  _dvm_alias_set "$@"
}

dvm_alias_delete() {
  if [[ $# < 1 ]]; then
    echo "usage: dvm alias delete <alias>"
    return 1
  fi
  if [[ ! -h "$DVM_ROOT/darts/$1" ]]; then
    echo "ERROR: no such alias $1."
    return 1
  fi
  rm -f -- "$DVM_ROOT/darts/$1"
}

dvm_alias() {
  if [[ $# < 1 ]]; then
    _dvm_alias_usage
    return 1
  fi
  cmd=$1
  shift
  case $cmd in
    create)
      dvm_alias_create "$@"
      ;;
    delete)
      dvm_alias_delete "$@"
      ;;
    list)
      dvm_alias_list "$@"
      ;;
    update)
      dvm_alias_update "$@"
      ;;
    *)
      _dvm_alias_usage
      return 1
      ;;
  esac
}

dvm_use() {
  if [[ $# < 1 ]]; then
    echo "usage: dvm use <version> [--default]"
    return 1
  fi

  local version=$1
  local default=$2
  shift

  if [[ ! -e "$DVM_ROOT/darts/$version" ]]; then
    echo "ERROR: version not found. Try 'dvm install $version'."
    return 1
  fi

  if [[ "$default" == "--default" ]]; then
    local defaults="$DVM_ROOT/environments/default"
    echo 'export DVM_ROOT; DVM_ROOT="$DVM_ROOT"' > "$defaults"
    echo "export DART_SDK; DART_SDK=\"$DVM_ROOT/darts/$version\"" >> "$defaults"
    echo "PATH=\"\$DVM_ROOT/darts/$version/bin:\$PATH\"" >> "$defaults"
  fi

  export DART_SDK="$DVM_ROOT/darts/$version"
  _dvm_path_prepend "$DVM_ROOT/darts/$version/bin"
}

dvm_list() {
  find "$DVM_ROOT/darts" -maxdepth 1 ! -path "$DVM_ROOT/darts" -type d \
      -exec basename "{}" \; | sort -V
}

_dvm_list_repo() {
  local channel=$1
  local api_uri="https://www.googleapis.com/storage/v1/b/dart-archive/o"
  local query="prefix=channels/$channel/release/&delimiter=/"
  curl -s "$api_uri?$query" | \
      grep "channels/$channel/release/" | \
      sed -e "s@.*/$channel/release/@@;s@/.*@@" | \
      grep -v "^[0-9]*$" | \
      sort -V
}

dvm_listall() {
  if [[ "$1" == "--dev" ]]; then
    _dvm_list_repo "dev"
  elif [[ "$1" == "--beta" ]]; then
    _dvm_list_repo "beta"
  else
    _dvm_list_repo "stable"
  fi
}

_dvm_download_sdk() {
  local channel=$1
  local version=$2
  local sdk_archive=$3

  local suffix
  if [[ "$channel" == "main" ]]; then
    suffix="raw"
  else
    suffix="release"
  fi

  local dl_uri="https://storage.googleapis.com/dart-archive/channels/$channel/$suffix"
  local base_uri="$dl_uri/$version"
  echo "Downloading: $base_uri/sdk/$sdk_archive"
  curl -f -O "$base_uri/sdk/$sdk_archive"
}

_dvm_download_content_shell() {
  local channel=$1
  local version=$2
  local content_shell_archive=$3

  local dl_uri="https://storage.googleapis.com/dart-archive/channels/$channel/release"
  local base_uri="$dl_uri/$version"
  echo "Downloading: $base_uri/dartium/$content_shell_archive"
  curl -f -O "$base_uri/dartium/$content_shell_archive"
}

_dvm_download_dartium() {
  local channel=$1
  local version=$2
  local dartium_archive=$3

  local dl_uri="https://storage.googleapis.com/dart-archive/channels/$channel/release"
  local base_uri="$dl_uri/$version"
  echo "Downloading: $base_uri/dartium/$dartium_archive"
  curl -f -O "$base_uri/dartium/$dartium_archive"
}

# Returns the CPU architechture for Dart in the specified SDK version.
#
# Used by Apple Silicon (arm64) based Macs to determine the architecture of the
# Dart SDK archive to download. For SDK 2.14.1 and later, download the arm64
# bundle. Prior to that, only Intel binaries are available.
_macos_arm64_sdk_arch() {
  local archBoundary="2.14.1"
  if [[ "$1" == "latest" || "$(printf "$1\n$archBoundary\n" | sort -t. -n -k 1,1 -k 2,2 -k 3,3 | head -n1)" == "$archBoundary" ]]; then
    echo "arm64"
  else
    echo "x64"
  fi
}

# Returns the CPU architechture for Dartium in the specified SDK version.
#
# In Dart SDKx 1.19.x and earlier, Dartium shipped only as an ia32 build. In
# SDKs later than 1.20.0, it was produced as an x64 build. Through a happy
# quirk of fate, 1.20.0 never existed.
_macos_dartium_arch() {
  local archBoundary="1.20.0"
  if [[ "$1" == "latest" || "$(printf "$1\n$archBoundary\n" | sort -t. -n -k 1,1 -k 2,2 -k 3,3 | head -n1)" == "$archBoundary" ]]; then
    echo "x64"
  else
    echo "ia32"
  fi
}

dvm_install() {
  if [[ $# < 1 ]]; then
    echo "usage: dvm install <version> [channel]"
    return 1
  fi

  curl=$(which curl)
  if [[ ! -x "$curl" ]]; then
    echo "ERROR: curl is required but was not found on PATH."
    return 1
  fi

  local version=$1
  local channel=$2
  shift

  if [[ -d "$DVM_ROOT/darts/$version" ]]; then
    echo "ERROR: version $version is already installed."
    return 1
  fi

  case $(uname -a) in
    Darwin*arm64*)
      local arch="$(_macos_arm64_sdk_arch "$version")"
      local sdk_archive="dartsdk-macos-$arch-release.zip"
      arch="$(_macos_dartium_arch "$version")"
      local content_shell_archive="content_shell-macos-$arch-release.zip"
      local dartium_archive="dartium-macos-$arch-release.zip"
      ;;
    Darwin*)
      local arch="$(_macos_dartium_arch "$version")"
      local sdk_archive="dartsdk-macos-x64-release.zip"
      local content_shell_archive="content_shell-macos-$arch-release.zip"
      local dartium_archive="dartium-macos-$arch-release.zip"
      ;;
    Linux*armv7*)
      local sdk_archive=dartsdk-linux-arm-release.zip
      local content_shell_archive=content_shell-linux-arm-release.zip
      local dartium_archive=dartium-linux-arm-release.zip
      ;;
    Linux*armv8*)
      local sdk_archive=dartsdk-linux-arm64-release.zip
      local content_shell_archive=content_shell-linux-arm64-release.zip
      local dartium_archive=dartium-linux-arm64-release.zip
      ;;
    Linux*)
      local sdk_archive=dartsdk-linux-x64-release.zip
      local content_shell_archive=content_shell-linux-x64-release.zip
      local dartium_archive=dartium-linux-x64-release.zip
      ;;
    *)
      echo "ERROR: unable to determine OS."
      return 1
      ;;
  esac

  # Create tmp workspace.
  tmpdir=$(mktemp -d)
  pushd $tmpdir > /dev/null

  # Download SDK.
  # Use channel if defined, otherwise loop through all channels.
  if [[ -z "$channel" ]]; then
    for channel in "stable" "beta" "dev"; do
      _dvm_download_sdk "$channel" "$version" "$sdk_archive" && break
    done
  else
    _dvm_download_sdk "$channel" "$version" "$sdk_archive"
  fi

  if [[ $? -ne 0 ]]; then
    echo "ERROR: unable to download Dart SDK. Are you sure that version exists?"
    popd > /dev/null
    rm -rf -- "$tmpdir"
    return 1
  fi
  unzip $sdk_archive
  version=$(<dart-sdk/version)

  # Move SDK under $DVM_ROOT/darts.
  if [[ -d "$DVM_ROOT/darts/$version" ]]; then
    rm -rf -- "$DVM_ROOT/darts/$version"
  fi
  mv dart-sdk "$DVM_ROOT/darts/$version"

  # Content shell/Dartium are not included with Dart 2.0 onwards.
  if [[ "$version" = "1."* ]]; then
    # Download Content Shell
    for channel in "stable" "beta" "dev"; do
      _dvm_download_content_shell "$channel" "$version" "$content_shell_archive" && break
    done
    if [[ $? -ne 0 ]]; then
      echo "ERROR: unable to download Content Shell. But hey, at least you got the SDK."
    else
      unzip $content_shell_archive
      if [[ ! -d "$DVM_ROOT/darts/$version/content_shell" ]]; then
        local content_shell_dir="$(find . -maxdepth 1 ! -path . -type d)"
        mv "$content_shell_dir" "$DVM_ROOT/darts/$version/content_shell"
      fi

      # Create Content Shell symlink.
      pushd "$DVM_ROOT/darts/$version/bin" > /dev/null
      case $(uname) in
        Darwin)
          echo "#!/bin/bash" > content_shell
          echo "exec \"$DVM_ROOT/darts/$version/content_shell/Content Shell.app/Contents/MacOS/Content Shell\" \"\$@\"" >> content_shell
          chmod ugo+x content_shell
          ;;
        Linux)
          ln -s ../content_shell/content_shell content_shell
          ;;
      esac
      popd > /dev/null
    fi

    # Download Dartium.
    for channel in "stable" "beta" "dev"; do
      _dvm_download_dartium "$channel" "$version" "$dartium_archive" && break
    done
    if [[ $? -ne 0 ]]; then
      echo "ERROR: unable to download Dartium. But hey, at least you got the SDK."
    else
      unzip $dartium_archive
      if [[ ! -d "$DVM_ROOT/darts/$version/dartium" ]]; then
        local dartium_dir="$(find . -maxdepth 1 ! -path . -type d)"
        mv "$dartium_dir" "$DVM_ROOT/darts/$version/dartium"
      fi

      # Create Dartium symlink.
      pushd "$DVM_ROOT/darts/$version/bin" > /dev/null
      case $(uname) in
        Darwin)
          echo "#!/bin/bash" > dartium
          echo "open \"$DVM_ROOT/darts/$version/dartium/Chromium.app\"" >> dartium
          chmod ugo+x dartium
          ;;
        Linux)
          ln -s ../dartium/chrome dartium
          ;;
      esac
      popd > /dev/null
    fi
  fi

  # Clean up.
  popd > /dev/null
  rm -rf -- "$tmpdir"
}

# Returns 1 if there are differences against the latest upstream version;
# otherwise returns 0.
_dvm_needsupgrade() {
  git -C "$DVM_ROOT" fetch origin
  if [[ $? -ne 0 ]]; then
    # Failed to fetch; don't try to update.
    echo "ERROR: failed to fetch dvm."
    return 0
  fi

  local local_sha="$(git -C "$DVM_ROOT" rev-parse HEAD)"
  local remote_sha="$(git -C "$DVM_ROOT" rev-parse origin/master)"
  if [[ "$local_sha" == "$remote_sha" ]]; then
    # No diffs vs upstream.
    return 0
  fi
  return 1
}

dvm_upgrade() {
  # Abort if there are local diffs that aren't checked in.
  local diffs="$(git -C "$DVM_ROOT" status --porcelain=v1 2>/dev/null)"
  if [[ -n "$diffs" ]]; then
    echo "ERROR: aborting update. Local changes found: $DVM_ROOT"
    echo "Please stash or commit your changes prior to updating."
    return 1
  fi

  _dvm_needsupgrade
  if [[ $? -eq 0 ]]; then
    echo "dvm is up to date."
    return 0
  fi

  git -C "$DVM_ROOT" pull --rebase origin master > /dev/null
  if [[ $? -ne 0 ]]; then
    git -C "$DVM_ROOT" rebase --abort > /dev/null
    echo "ERROR: failed to upgrade dvm."
    return 1
  fi

  # Re-source the script to replace function definitions.
  source "$DVM_ROOT/scripts/dvm"
  echo "dvm was upgraded to the latest version."
  return 0
}

dvm_version() {
  echo "Dart Version Manager version $DVM_VERSION installed at $DVM_ROOT"
}

dvm_implode() {
  echo "This will delete dvm and all installed versions."
  echo -n "Are you sure? "
  read yn
  yn=$(tr '[:upper:]' '[:lower:]' <<< "$yn")
  if [[ "$yn" == "y" || "$yn" == "yes" ]]; then
    rm -rf -- "$DVM_ROOT" && echo "DVM successfully removed." || echo "Failed to remove DVM."
  else
    echo "Cancelled."
  fi
}

dvm() {
  # Wrap with subshell to avoid changing the shell's current directory.
  (_dvm "$@")
}

_dvm() {
  if [[ -z "$DVM_ROOT" ]]; then
    echo "ERROR: DVM_ROOT not set. Please source \$DVM_ROOT/scripts/dvm."
    return 1
  fi
  if [[ ! -d "$DVM_ROOT" ]]; then
    echo "ERROR: DVM_ROOT does not exist. Please reinstall dvm."
    return 1
  fi

  # Verify prerequisites are installed.
  _dvm_doctor
  if [[ $? -ne 0 ]]; then
    return 1
  fi

  # Create required directories.
  _dvm_create_dirs

  if [[ $# < 1 ]]; then
    _dvm_usage
    return 1
  fi
  cmd=$1
  shift
  case $cmd in
    alias)
      dvm_alias "$@"
      ;;
    implode)
      dvm_implode "$@"
      ;;
    install)
      dvm_install "$@"
      ;;
    list)
      dvm_list "$@"
      ;;
    listall)
      dvm_listall "$@"
      ;;
    upgrade)
      dvm_upgrade "$@"
      ;;
    use)
      dvm_use "$@"
      ;;
    version)
      dvm_version "$@"
      ;;
    *)
      if [[ $(type -t dvm_$cmd) == "function" ]]; then
        dvm_$cmd "$@"
      else
        _dvm_usage
      fi
      ;;
  esac
}

if [[ -e "$DVM_ROOT/environments/default" ]]; then
  . "$DVM_ROOT/environments/default"
fi
